using System;
using System.ComponentModel;
using System.Runtime.Serialization;

namespace Ns.Data.Entity
{
    /// <summary>
    /// Represents a type whose relationship with its parent requires change tracking.
    /// </summary>
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
#if !SILVERLIGHT
    [Serializable]
#endif
    [DataContract(Name = "EntityItemOf{0}")]
    public sealed class EntityItem<TEntity>
        : IPersistEntity
    {
        private const string PersistChangePropertyName = "PersistChange";
        private const string NotSupported_InvalidPersistChange = "{0} is not a valid PersistChange of an EntityItem.";
        private const string InvalidOperation_InconsistentPersistChange = "{0} and {1} represent inconsistent PersistChange between EntityItem and Entity.";
        private PersistEntity _persistEntity;
        private TEntity _entity;

        /// <summary>
        /// Gets the entity.
        /// </summary>
        /// <value>The entity.</value>
        [DataMember(EmitDefaultValue = false)]
        public TEntity Entity
        {
            get { return _entity; }
            set
            {
                _entity = value;
                if (_persistEntity != null)
                    _persistEntity.PropertyChanged -= Entity_PropertyChanged;
                _persistEntity = value as PersistEntity;
                if (_persistEntity != null)
                    _persistEntity.PropertyChanged += new PropertyChangedEventHandler(Entity_PropertyChanged);
            }
        }

        private PersistChange _persistChange;

        /// <summary>
        /// Gets or sets the persist change.
        /// </summary>
        /// <value>The persist change.</value>
        [DataMember(EmitDefaultValue = false)]
        public PersistChange PersistChange
        {
            get { return _persistChange; }
            set
            {
                if (_persistChange != value)
                {
                    //consider invalid PersistChange
                    if (value == PersistChange.Update)
                        throw new NotSupportedException(string.Format(NotSupported_InvalidPersistChange, value));

                    _persistChange = value;
                    OnPropertyChanged(PersistChangePropertyName);
                }
            }
        }

        #region Constructors

        private EntityItem()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="EntityItem&lt;TEntity&gt;"/> class 
        /// with the <see cref="PersistEntity.CurrentPersistChange"/> 
        /// and according to the <see cref="PersistEntity.CurrentChangeTracking"/>.
        /// </summary>        
        /// <param name="entity">The entity.</param>
        /// <exception cref="ArgumentNullException"><paramref name="entity"/> is null.</exception>
        public EntityItem(TEntity entity)
            : this(entity
            , PersistEntity.CurrentPersistChange
            ?? ((PersistEntity.CurrentChangeTracking == ChangeTracking.Enabled)
                ? PersistChange.Insert
                : PersistChange.None))
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="EntityItem&lt;TEntity&gt;"/> class
        /// with the specified <see cref="PersistChange"/>.
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <param name="persistChange">The persist change.</param>
        /// <exception cref="ArgumentNullException"><paramref name="entity"/> is null.</exception>
        public EntityItem(TEntity entity, PersistChange persistChange)
        {
            if (entity == null)
                throw new ArgumentNullException("entity");
            _entity = entity;
            _persistEntity = entity as PersistEntity;
            if (_persistEntity != null)
                _persistEntity.PropertyChanged += new PropertyChangedEventHandler(Entity_PropertyChanged);
            _persistChange = persistChange;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="EntityItem&lt;TEntity&gt;"/> class
        /// copied from the specified <paramref name="item"/>.
        /// </summary>
        /// <param name="item">The item.</param>
        /// <remarks>This constructor ignores any <see cref="ChangeTrackingScope"/> and <see cref="PersistChangeScope"/>.</remarks>
        /// <exception cref="ArgumentNullException"><paramref name="item"/> is null.</exception>
        public EntityItem(EntityItem<TEntity> item)
        {
            if (item == null)
                throw new ArgumentNullException("item");
            this._entity = item._entity;
            this._persistChange = item._persistChange;
            this._persistEntity = item._persistEntity;
        }

        #endregion

        private void Entity_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            //consider only necessary cases
            if (!string.Equals(e.PropertyName, PersistChangePropertyName))
                return;

            //consider PersistChange inconsistency
            if ((PersistChange == PersistChange.Insert && _persistEntity.PersistChange == PersistChange.Delete) ||
                (PersistChange == PersistChange.None && _persistEntity.PersistChange == PersistChange.Delete))
            {
                throw new InvalidOperationException(string.Format(InvalidOperation_InconsistentPersistChange, PersistChange, _persistEntity.PersistChange));
            }
        }

        #region Overriden Members

        /// <summary>
        /// Converts the value of the current <see cref="EntityItem&lt;TEntity&gt;"/> object to its equivalent string representation. 
        /// </summary>
        /// <returns>A string representation of the value of the current <see cref="EntityItem&lt;TEntity&gt;"/> object.</returns>        
        public override string ToString()
        {
            return string.Format("PersistChange = {0}, Entity = {1}",
                PersistChange,
                (Entity != null)
                    ? Entity.ToString()
                    : string.Empty);
        }

        #endregion

        #region Implicit / Explicit Operators

        /// <summary>
        /// Performs an implicit conversion from TEntity to <see cref="EntityItem&lt;TEntity&gt;"/>.
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <returns>The result of the conversion.</returns>
        public static explicit operator EntityItem<TEntity>(TEntity entity)
        {
            return new EntityItem<TEntity>(entity);
        }

        /// <summary>
        /// Performs an implicit conversion from <see cref="EntityItem&lt;TEntity&gt;"/> to TEntity.
        /// </summary>
        /// <param name="item">The item.</param>
        /// <returns>The result of the conversion.</returns>
        public static implicit operator TEntity(EntityItem<TEntity> item)
        {
            return (item != null)
                ? item.Entity
                : default(TEntity);
        }

        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Occurs after the value of a property is changed.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Raises the <see cref="E:PersistEntity.PropertyChanged"/> event.
        /// </summary>
        /// <param name="propertyName">The name of the property whose value has changed.</param>
        void OnPropertyChanged(string propertyName)
        {
            OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
        }

        /// <summary>
        /// Raises the <see cref="E:PersistEntity.PropertyChanged"/> event.
        /// </summary>
        /// <param name="e">A <see cref="T:System.ComponentModel.PropertyChangedEventArgs"/> that contains the event data.</param>
        void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            //keep handler to avoid race condition
            var handler = PropertyChanged;
            if (handler != null)
                handler(this, e);
        }

        #endregion
    }
}